// Created by Windward Studios - no copyright is claimed. This code can be used in
// any manner by anyone for any reason. There is no copyright of any kind on it. You may
// use it in commercial products. You may change it without sharing those changes.
// We ask that you keep the "created by Windward Studios" in a comment at the top.


using System;
using System.IO;
using System.Net;
using System.Text;
using System.Text.RegularExpressions;
using Kailua.WSSAuthentication;
using Kailua.WSSCopy;
using net.windward.utils;

namespace Kailua.net.windward.utils
{
	/// <summary>
	/// The methods missing from Path and File classes. A lot of these are designed to handle both files
	/// and urls. For most methods the way a url is handled is to do nothing.
	/// </summary>
	public class FileUtils
	{
		/// <summary>
		/// What type of filename this is. This uses flags so you can test the return value to see if
		/// it is any of several acceptable results.
		/// </summary>
		[Flags]
		public enum FilenameType : byte
		{
			/// <summary>
			/// The file is accessed using a c:\dir\filename.
			/// </summary>
			drive = 0x01,
			/// <summary>
			/// The file is accessed using a \\server\share\filename.
			/// </summary>
			unc = 0x02,
			/// <summary>
			/// The file is accessed using http:filename or ftp:filename.
			/// </summary>
			url = 0x04,
			/// <summary>
			/// The filename is null or 0 length.
			/// </summary>
			none = 0x10,
			/// <summary>
			/// Is inside a jar or zip file.
			/// </summary>
			zip = 0x20,
			/// <summary>
			/// Is inside the Office 2007 template file.
			/// </summary>
			self = 0x40,
			/// <summary>
			/// The filename uses some other mechanism.
			/// </summary>
			other = 0x80
		} ;

		private static readonly char[] separators = { Path.DirectorySeparatorChar, Path.VolumeSeparatorChar };
		private const string zipRegex = "^(jar|zip):";
		private const string uriRegex = "^([^!]+)!(/[^!]+)$";

		/// <summary>
		/// Returns the type of filename. This is not perfect because Windows does not provide all the info
		/// we need to do this right.
		/// </summary>
		/// <param name="filename">The filename to check.</param>
		/// <returns>true if a filesystem (not http, ...)</returns>
		public static FilenameType GetFilesystemType(string filename)
		{

			if (string.IsNullOrEmpty(filename))
				return FilenameType.none;

			// first http and ftp
			filename = filename.ToLower();


			if (filename.Contains("workspace.office.live"))
				return FilenameType.unc;
			if (filename.StartsWith("http:") || filename.StartsWith("ftp:") || filename.StartsWith("file:") ||
				(filename.StartsWith("https:")))
				return FilenameType.url;
			if (filename.StartsWith("self:"))
				return FilenameType.self;

			// now unc
			if (filename.StartsWith("\\") && (filename.Length > 3) && (filename[2] != '\\'))
				return FilenameType.unc;

			// if we have no VolumeSeparatorChar or it's at [1] then we're a filename
			int pos = filename.IndexOf(Path.VolumeSeparatorChar);
			if ((pos == -1) || (pos == 1))
				return FilenameType.drive;

			// zip check
			if (Regex.Match(filename, zipRegex).Success)
			{
				string stripped = Regex.Replace(filename, zipRegex, "");
				if (Regex.Match(stripped, uriRegex).Success)
					return FilenameType.zip;
			}

			// and now we give up
			return FilenameType.other;
		}

		/// <summary>
		/// Open a stream for this file. Handles all file types.
		/// </summary>
		/// <param name="filename">The filename.</param>
		/// <returns>The stream for the file.</returns>
		public static Stream GetStream(string filename)
		{

			switch (GetFilesystemType(filename))
			{
				case FilenameType.url:
					WebClient client = new WebClient();
					client.Credentials = CredentialCache.DefaultNetworkCredentials;
					return client.OpenRead(filename);
				default:
					return new FileStream(filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
			}
		}

		/// <summary>
		/// If the filename is http, etc will return null. Otherwise returns the path part of the passed in filename.
		/// This is useful when passing an existing filename to the FileOpen dialog and it might be a url.
		/// </summary>
		/// <param name="filename">The filename to check.</param>
		/// <returns>null or the path.</returns>
		public static string PathOrNull(string filename)
		{
			if ((GetFilesystemType(filename) & (FilenameType.drive | FilenameType.unc)) != 0)
			{
				string path = Path.GetDirectoryName(Path.GetFullPath(filename));
				if (!string.IsNullOrEmpty(path))
					return path;
			}
			return null;
		}

		/// <summary>
		/// If the filename is http, etc will return null. Otherwise returns the passed in filename.
		/// This is useful when passing an existing filename to the FileOpen dialog and it might be
		/// a url.
		/// </summary>
		/// <param name="filename">The filename to check.</param>
		/// <returns>null or the filename.</returns>
		public static string FilenameOrNull(string filename)
		{
			if ((GetFilesystemType(filename) & (FilenameType.drive | FilenameType.unc)) != 0)
			{
				filename = Path.GetFileName(filename);
				if (!string.IsNullOrEmpty(filename))
					return filename;
			}
			return null;
		}

		/// <summary>
		/// Does a Path.GetFullPath if filename is a filesystem file. Otherwise just returns filename.
		/// </summary>
		/// <param name="filename">The filename to check.</param>
		/// <returns>the full path if a filesystem file. Otherwise the passed in filename.</returns>
		public static string FullPath(string filename)
		{
			if (string.IsNullOrEmpty(filename) || (!((GetFilesystemType(filename) & (FilenameType.drive | FilenameType.unc)) != 0)))
				return filename;
			return Path.GetFullPath(filename);
		}

		/// <summary>
		/// Does a Path.GetFullPath(Path.Combine(rootdirectory, filename)) if filename is a filesystem file. Otherwise just returns filename.
		/// </summary>
		/// <param name="rootdirectory">The root directory if a relative filename.</param>
		/// <param name="filename">The filename to check.</param>
		/// <returns>the full path if a filesystem file. Otherwise the passed in filename.</returns>
		public static string CombineFullPath(string rootdirectory, string filename)
		{

			if ((filename == null) || ((GetFilesystemType(filename) & (FilenameType.drive | FilenameType.unc)) == 0))
				return filename;
			if (Path.IsPathRooted(filename))
				return filename;
			return Path.GetFullPath(Path.Combine(rootdirectory, filename));
		}

		/// <summary>
		/// Convert to a relative filename.
		/// </summary>
		/// <param name="root">The root to be relative to.</param>
		/// <param name="filename">The fully qualified filename.</param>
		/// <returns>The relative filename.</returns>
		public static string RelativePath(string root, string filename)
		{

			if (string.IsNullOrEmpty(filename) || string.IsNullOrEmpty(root))
				return filename;
			if ((GetFilesystemType(filename) & (FilenameType.drive | FilenameType.unc)) == 0)
				return filename;
			if (!Path.IsPathRooted(filename))
				return filename;

			string[] bPath = root.Split(separators);
			string[] aPath = filename.Split(separators);
			int indx = 0;
			for (; indx < Math.Min(bPath.Length, aPath.Length); ++indx)
				if (!bPath[indx].Equals(aPath[indx]))
					break;

			if (indx == 0)
				return filename;

			string erg = "";

			if (indx == bPath.Length)
				erg += "." + Path.DirectorySeparatorChar;
			else
				for (int i = indx; i < bPath.Length; ++i)
					erg += ".." + Path.DirectorySeparatorChar;
			erg += String.Join(Path.DirectorySeparatorChar.ToString(), aPath, indx, aPath.Length - indx);
			return erg;
		}

		/// <summary>
		/// Checks if the filename is a legit filename (to stop security threats).
		/// </summary>
		/// <param name="filename">The filename to check. This file does not need to exist.</param>
		/// <returns>True if this is a legit filename.</returns>
		public static bool IsLegitFilename(string filename)
		{

			// we do nothing for web files (yet).
			if (filename.ToLower().StartsWith("http://") || filename.ToLower().StartsWith("ftp://"))
				return true;

			filename = MakeCanonical(filename);
			if (filename.IndexOfAny(Path.GetInvalidPathChars()) != -1)
				return false;

			return true;
		}

		/// <summary>
		/// Convert ${windir} and ${sysdir} to the actual path in a filename.
		/// </summary>
		/// <param name="filename">The filename to convert.</param>
		/// <returns>The converted filename.</returns>
		public static string ConvertMacros(string filename)
		{

			if (filename == null)
				return null;
			filename = filename.Replace("${sysdir}", Environment.SystemDirectory);
			filename = filename.Replace("${windir}", Path.Combine(Environment.SystemDirectory, ".."));
			try
			{
				return Path.GetFullPath(filename);
			}
			catch (Exception)
			{
				return filename;
			}
		}

		/// <summary>
		/// Convert filename into a cannonical representation of the filename. Do not pass a url to this method.
		/// </summary>
		/// <param name="filename">The filename to convert.</param>
		/// <returns>The canonical filename.</returns>
		public static string MakeCanonical(string filename)
		{

			// this can fail for things like \\ instead of \\server\share
			try
			{
				filename = Path.GetFullPath(filename);
			}
			catch (Exception)
			{
				// do nothing
			}

			// change / to \
			filename = filename.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
			// lowercase everything (this is not perfect if it's a unix share)
			return filename.ToLower();
		}

		/// <summary>
		/// Converts a mapped drive like n: to the unc \\server\share syntax. Returns passed in file full qualified
		/// if not a mapped drive.
		/// </summary>
		/// <param name="filename">filename to convert.</param>
		/// <returns>filename using unc instead of a drive letter.</returns>
		public static string ConvertToUNC(string filename)
		{

			// bugbug - use PathIsNetworkPath, WNetGetConnection, PathIsUNC
			return FullPath(filename);
		}

		private static readonly char[] invalid = { '/', '\\', '.', ':', '|', '>', '<', '*', '?' };

		/// <summary>
		/// Convert the name to a valid file name.
		/// </summary>
		/// <param name="name">The name to convert.</param>
		/// <param name="noSpaces">If true spaces will be converted to an underscore. If false, it still will not allow spaces at the ends of the string.</param>
		/// <returns>The converted name.</returns>
		public static string MakeValidFileName(string name, bool noSpaces)
		{

			if (noSpaces)
				name = name.Replace(' ', '_');
			StringBuilder buf = new StringBuilder();
			foreach (char ch in name)
			{
				bool canAdd = ch >= 32;
				if (canAdd)
					foreach (char chOn in invalid)
						if (ch == chOn)
						{
							canAdd = false;
							break;
						}
				if (canAdd)
					foreach (char chOn in Path.GetInvalidFileNameChars())
						if (ch == chOn)
						{
							canAdd = false;
							break;
						}
				if (canAdd)
					buf.Append(ch);
			}

			string rtn = buf.ToString().Trim();
			return rtn.Length == 0 ? "_" : rtn;
		}

		/// <summary>
		/// Read a file in a SharePoint list. This works on forms based authentication.
		/// </summary>
		/// <param name="filename">The url of the SharePoint file.</param>
		/// <param name="username">The username of the credentials.</param>
		/// <param name="password">The password of the credentials.</param>
		/// <returns>The contents of the file.</returns>
		public static byte[] ReadSharePointFile(string filename, string username, string password)
		{
			byte[] fileContents;
			using (Authentication authSvc = new Authentication())
			{
				Uri uriBase = new Uri(filename);

				if ((!string.IsNullOrEmpty(username)) || (!string.IsNullOrEmpty(password)))
				{
					uriBase = new Uri(uriBase.GetLeftPart(UriPartial.Authority));
					Uri uriLogin = new Uri(uriBase, "_vti_bin/authentication.asmx");
					authSvc.Url = uriLogin.AbsoluteUri;
					authSvc.CookieContainer = new CookieContainer();
					authSvc.AllowAutoRedirect = true;

					// set the FBA login information; login to the FBA site and store the result
					LoginResult result = authSvc.Login(username, password);
					if (result.ErrorCode != LoginErrorCode.NoError)
					{
						Trap.trap();
						throw new ApplicationException(string.Format("Could not login to site, error: {0}", result.ErrorCode));
					}
				}

				// download a document
				using (Copy copySvc = new Copy())
				{
					Uri uriCopy = new Uri(uriBase, "_vti_bin/copy.asmx");
					copySvc.Url = uriCopy.AbsoluteUri;

					// pass the FBA cookie
					copySvc.CookieContainer = authSvc.CookieContainer;

					// get the document
					FieldInformation[] fieldInfoArray;
					uint getItemResult = copySvc.GetItem(filename, out fieldInfoArray, out fileContents);
					if (getItemResult != 0)
					{
						Trap.trap();
						throw new ApplicationException(string.Format("Could not read the file, error: {0}", getItemResult));
					}
				}
			}
			return fileContents;
		}

		/// <summary>
		/// Return all files that match the spec.
		/// </summary>
		/// <param name="fileSpec">A path with wildcards allowed in the filename part.</param>
		/// <returns>All files that match the spec.</returns>
		public static string[] GetAllFiles(string fileSpec)
		{

			if (File.Exists(fileSpec))
				return new string[] { fileSpec };

			if (Directory.Exists(fileSpec))
			{
				Trap.trap();
				return Directory.GetFiles(fileSpec);
			}

			int pos1 = fileSpec.LastIndexOf('/');
			int pos2 = fileSpec.LastIndexOf('\\');
			int pos = pos1 == -1 ? pos2 : (pos2 == -1 ? pos1 : Math.Max(pos1, pos2));
			if (pos == -1)
				return Directory.GetFiles(".", fileSpec);
			Trap.trap();
			return Directory.GetFiles(fileSpec.Substring(0, pos), fileSpec.Substring(pos + 1));
		}
	}
}